<?php

 /**
  * @package JooS
  * @subpackage PHPackager_FS
  */

  final class JooS_PHPackager_FS {
    const DIRECTORY_SEPARATOR = "/";

    const EVENT_NEW_FILE = "FileSystem.newFile";

    const EVENT_DELETE_FILE = "FileSystem.deleteFile";

    /**
     * @var JooS_PHPackager_FS
     */
    private static $_instance = null;

    private $_root = null;

    private $_entities = null;

    private $_actions = null;

    /**
     * @param string $root
     * @return JooS_PHPackager_FS
     */
    public static function startTransaction($root = null) {
      if (self::$_instance === null) {
        if ($root === null) {
          require_once "JooS/Config.php";

          $root = JooS_Config::PHPackager()->path();
        }

        self::$_instance = new self($root);
        return self::$_instance;
      }
      else
        self::throwException("New FS-transaction could not be started. You must rollback or commit previous FS-transaction");
    }

    public static function rollbackTransaction() {
      if (self::$_instance !== null)
        self::$_instance = null;
    }
    
    public static function commitTransaction() {
      $_instance = self::getInstance();
      if ($_instance === null)
        self::throwException("FS-transaction was not started yet");

      foreach ($_instance->_actions as $action) {
        $action();
      }
      self::$_instance = null;
    }

    /**
     * @return JooS_PHPackager_FS
     */
    public static function getInstance() {
      return self::$_instance;
    }

    /**
     * @param string $root
     */
    private function __construct($root) {
      $this->_root = $this->_getNormalizedPath($root);
      $this->_entities = array();
      $this->_actions = array();
    }

    /**
     * @param string $path
     * @param int $mode
     */
    public function chmod($path, $mode) {
      $entity = $this->getEntity($path);
      $realPath = $entity->getPath();

      if ($entity->is_readable() && $entity->is_writable()) {
        $this->addAction(function() use ($realPath, $mode) {
          chmod($realPath, $mode);
        });
      }
      else
        self::throwException("File or Directory '$path' not found or must be writable");
    }

    /**
     * @param string $path
     * @param int $mode
     */
    public function mkdir($path, $mode = 0777) {
      $entity = $this->getEntity($path);
      $realPath = $entity->getPath();

      if ($entity->file_exists()) {
        self::throwException("Directory '$realPath' already exists");
      }
      else {
        $parentDir = $this->_getParentEntity($entity, true);

        $this
          ->addAction(function() use ($realPath) {
            mkdir($realPath);
          })
          ->addEntity($realPath, JooS_PHPackager_FS_Entity::ENTITY_DIR);

        $this->chmod($path, $mode);
      }
    }

    /**
     * @param string $path
     * @param JooS_CodeGenerator_File_Adapter $contents
     * @return JooS_CodeGenerator_File_Adapter
     */
    public function file_put_contents($path, $contents) {
      $entity = $this->getEntity($path);
      $realPath = $entity->getPath();

      if ($entity->file_exists()) {
        self::throwException("File '$realPath' already exists");
      }
      else {
        $parentDir = $this->_getParentEntity($entity, true);

        require_once "JooS/CodeGenerator/File.php";
        $tmpFile = JooS_CodeGenerator_File::createAdapter("Tmp");

        if ($contents instanceof JooS_CodeGenerator_File_Adapter)
          $contents->copy($tmpFile);
        else
          $tmpFile->file_put_contents($contents);

        $this
          ->addAction(function() use (&$tmpFile, $realPath) { 
            copy($tmpFile->get_name(), $realPath);
            unset($tmpFile);
          })
          ->addEntity($realPath, JooS_PHPackager_FS_Entity::ENTITY_FILE);

        return $tmpFile;
      }
    }

    /**
     *
     * @param string $source
     * @param string $path
     */
    public function copy($source, $path) {
      $realPath = $this->_getNormalizedPath(realpath($source));
      if (is_file($realPath)) {
        require_once "JooS/CodeGenerator/File.php";
        $sourceFile = JooS_CodeGenerator_File::createAdapter("File")->set_name($realPath);

        $tmpFile = $this->file_put_contents($path, $sourceFile);

        require_once "JooS/Event.php";
        JooS_Event::newInstance(self::EVENT_NEW_FILE)->notify($realPath, $path, $tmpFile);
      }
      elseif (is_dir($realPath)) {
        $objects = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($realPath), RecursiveIteratorIterator::SELF_FIRST);
        foreach ($objects as $name => $splFileInfo) {
          $realName = $this->_getNormalizedPath($name);
          $relativeName = $path . self::DIRECTORY_SEPARATOR . $this->_getRelativePath($realName, $realPath);

          if ($splFileInfo->isFile())
            $this->copy($realName, $relativeName);
          elseif (!$splFileInfo->isDir())
            self::throwException("'$realName' must be File");
        }
      }
      else
        self::throwException("'$source' must be File or Directory, or not found ");
    }

    /**
     *
     * @param string $source
     * @param bool $cleanupDirectories
     */
    public function delete($source, $cleanupDirectories = false) {
      $realPath = $this->_getNormalizedPath($source);

      $path = $this->_getRelativePath($realPath);
      $entity = $this->getEntity($path);

      if ($entity->is_readable() && $entity->is_writable()) {
        if ($entity->is_file()) {
          $this
            ->addAction(function() use ($realPath, $cleanupDirectories) {
              $dirname = dirname($realPath);
              unlink($realPath);

              if ($cleanupDirectories) {
                while (is_dir($dirname) && is_writable($dirname)) {
                  $files = glob($dirname . JooS_PHPackager_FS::DIRECTORY_SEPARATOR . "*");
                  if (!sizeof($files)) {
                    rmdir($dirname);
                    $dirname = dirname($dirname);
                  }
                  else
                    break;
                }
              }
            })
            ->addEntity($realPath, JooS_PHPackager_FS_Entity::ENTITY_NONE);

          require_once "JooS/Event.php";
          JooS_Event::newInstance(self::EVENT_DELETE_FILE)->notify($realPath);
        }
        elseif ($entity->is_dir()) {
          foreach ($this->_entities as $_path => $_entity) {
            if ($_path != $realPath && substr($_path, 0, strlen($realPath)) == $realPath) {
              if ($_entity instanceof JooS_PHPackager_FS_Entity_None)
                continue;
              else
                return;
            }
          }

          if (file_exists($realPath)) {
            $files = glob(rtrim($realPath, self::DIRECTORY_SEPARATOR) . self::DIRECTORY_SEPARATOR . "*");
            foreach ($files as $_path) {
              $_path = $this->_getNormalizedPath($_path);
              if (!isset($this->_entities[$_path])) {
                return;
              }
            }
          }

          $this
            ->addAction(function() use ($realPath) {
              rmdir($realPath);
            })
            ->addEntity($realPath, JooS_PHPackager_FS_Entity::ENTITY_NONE);
        }
      }
    }

    /**
     *
     * @param string $realPath
     * @param string $type
     * @return JooS_PHPackager_FS 
     */
    protected function addEntity($realPath, $type) {
      $this->_entities[$realPath] = JooS_PHPackager_FS_Entity::getInstance($realPath, $type);

      return $this;
    }

    /**
     * @return JooS_PHPackager_FS
     */
    protected function addAction() {
      require_once "JooS/PHPackager/FS/Action.php";

      $args = func_get_args();
      $callback = array_shift($args);

      array_push($this->_actions, new JooS_PHPackager_FS_Action($callback, $args));

      return $this;
    }

    /**
     * @return string
     */
    public function getRoot() {
      return $this->_root;
    }

    /**
     *
     * @param string $path
     * @return JooS_PHPackager_FS_Entity
     */
    protected function getEntity($path) {
      require_once "JooS/PHPackager/FS/Entity.php";

      $realPath = $this->_getRealPath($path);
      $fsEntity = isset($this->_entities[$realPath]) ? $this->_entities[$realPath] : JooS_PHPackager_FS_Entity::getInstance($realPath);
      if ($fsEntity === null)
        self::throwException("FS_Entity class not implemented for $realPath");
      else
        return $fsEntity;
    }

    /**
     * @param string $path
     * @return string
     */
    public function _getNormalizedPath($path) {
      return str_replace("\\", self::DIRECTORY_SEPARATOR, $path);
    }

    /**
     *
     * @param JooS_PHPackager_FS_Entity $entity
     * @param bool $check
     * @return JooS_PHPackager_FS_Entity
     */
    private function _getParentEntity(JooS_PHPackager_FS_Entity $entity, $check = false) {
      $dirname = $entity->dirname();
      $entity = $this->getEntity( $this->_getRelativePath($dirname) );
      if ($check)
        $this->_checkParentEntity($entity);

      return $entity;
    }

    /**
     * @param JooS_PHPackager_FS_Entity $parentDir
     */
    private function _checkParentEntity(JooS_PHPackager_FS_Entity &$parentDir) {
      if (!$parentDir->file_exists()) {
        $path = $this->_getRelativePath($parentDir);
        $this->mkdir($path);
        $parentDir = $this->getEntity($path);
      }
      elseif (!$parentDir->is_dir())
        self::throwException("File '" . $parentDir->getPath(). "' must be Directory");

      if (!$parentDir->is_writable())
        self::throwException("Parent directory '" . $parentDir->getPath(). "' must be writable");
    }

    /**
     * @param string $path
     * @return string
     */
    private function _getRealPath($path) {
      $path = $this->_getNormalizedPath($path);
      $path = $this->_root . self::DIRECTORY_SEPARATOR . trim($path, self::DIRECTORY_SEPARATOR);

      $ds = str_repeat(self::DIRECTORY_SEPARATOR, 2);
      while (strpos($path, $ds) !== false)
        $path = str_replace($ds, self::DIRECTORY_SEPARATOR);

      return $path;
    }

    /**
     *
     * @param string $realPath
     * @param string $root
     * @return string
     */
    private function _getRelativePath($realPath, $root = null) {
      if (is_object($realPath) && $realPath instanceof JooS_PHPackager_FS_Entity)
        $realPath = $realPath->getPath();

      $root = $root === null ? $this->_root : $root;
      $pos = strpos($realPath, $root);

      if ($pos === 0)
        return substr($realPath, strlen($root) + 1);
      else
        self::throwException("Root path '" . $root . "' must contain real path '$realPath'");
    }

    /**
     * @param string $Message
     * @param int $Code
     * @throws JooS_PHPackager_FS_Exception
     */
    protected static function throwException($Message, $Code = 0) {
      require_once "JooS/PHPackager/FS/Exception.php";

      throw new JooS_PHPackager_FS_Exception($Message, $Code);
    }
  }
